package org.eclipse.gittab.player;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.eclipse.gittab.gitTabDsl.Beat;
import org.eclipse.gittab.gitTabDsl.Chord;
import org.eclipse.gittab.gitTabDsl.ChordDefinition;
import org.eclipse.gittab.gitTabDsl.DownStroke;
import org.eclipse.gittab.gitTabDsl.GitTabDslFactory;
import org.eclipse.gittab.gitTabDsl.GuitarSound;
import org.eclipse.gittab.gitTabDsl.Hold;
import org.eclipse.gittab.gitTabDsl.Note;
import org.eclipse.gittab.gitTabDsl.NoteValues;
import org.eclipse.gittab.gitTabDsl.Pause;
import org.eclipse.gittab.gitTabDsl.Sound;
import org.eclipse.gittab.gitTabDsl.Stroke;
import org.eclipse.gittab.gitTabDsl.Tab;
import org.eclipse.gittab.gitTabDsl.Tone;
import org.eclipse.gittab.gitTabDsl.Tuning;
import org.eclipse.gittab.gitTabDsl.UpStroke;
import org.eclipse.gittab.player.Composer.PickingMode;
import org.eclipse.gittab.player.internal.midi.MidiMapper;
import org.eclipse.gittab.player.internal.midi.MidiUtil;
import org.eclipse.gittab.player.internal.midi.Util;
import org.eclipse.gittab.util.GitTabUtil;

public class GitTabPlayer {

	// currently harmonics cannot be expressed in the Gittab DSL
	public enum EffectSounds {

		MUTED, HARMONICS
	}

	public static final int HIGH_VELOCITY = 117;
	public static final int NORMAL_VELOCITY = 93;
	public static final int LOW_VELOCITY = 87;

	private final Composer composer;
	private final Player player;

	private final Tuning tuning;

	public GitTabPlayer(Composer composer, Player player, Tuning tuning) {

		this.composer = composer;
		this.player = player;
		this.tuning = tuning;
	}

	public Player getPlayer() {
		return player;
	}

	protected final void load() {

		player.load(composer.getComposedTab(), tuning);
	}

	/**
	 * Plays all the {@link Beat}s in this {@link Tab}.
	 */
	public void load(Tab tab) {

		for (Beat beat : tab.getBeat()) {

			compose(beat);
		}

		load();
	}

	/**
	 * Plays the {@link Beat}.
	 */
	public void load(Beat beat) {

		compose(beat);

		load();
	}

	/**
	 * Plays a beat.
	 * 
	 * @param beat
	 */
	public void compose(Beat beat) {

		GuitarSound guitar = beat.getGuitar();
		int count = beat.getCount();

		if (count == 0)
			count = 1;

		if (guitar == GuitarSound.NULL) {
			// use the sound specified in the tuning
			Tab tab = Util.getContainerOfType(beat, Tab.class);
			guitar = tab.getGuitar();
		}

		while (count > 0) {
			for (Tone tone : beat.getNotes()) {
				compose(tone, guitar);
			}

			count--;
		}
	}

	private Sound lastSound;

	/**
	 * Plays a {@link Tone} with the given {@link GuitarSound}.
	 * 
	 * @param tone
	 * @param guitar
	 */
	public void compose(Tone tone, GuitarSound guitar) {

		int quaterValue = getQuaterValue(tone.getValue());

		List<Stroke> strokes = tone.getStrokes();

		int noteValue = quaterValue;

		// calculate the note value if strokes are available
		if (!strokes.isEmpty()) {
			noteValue *= strokes.size();
		}

		int instrument = MidiUtil.guitarToMidi(guitar);

		Sound sound = tone.getSound();
		// the user can also emit the sound of a tone, therefore just use the
		// last sound
		if (sound == null) {
			sound = lastSound;
		} else {
			lastSound = sound;
		}

		playSound(sound, tone.getStrokes(), noteValue, instrument);
	}

	/**
	 * Plays a single note with the given guitar sound and the length in ms.
	 * 
	 * @param note
	 * @param guitar
	 * @param duration
	 *            the duration in milli-seconds
	 */
	public void compose(ChordDefinition chord, GuitarSound guitar,
			int noteValue, List<Stroke> strokes) {

		Chord chordWrapper = GitTabDslFactory.eINSTANCE.createChord();
		chordWrapper.setChord(chord);

		composeSound(chordWrapper, noteValue, MidiUtil.guitarToMidi(guitar));
	}

	/**
	 * Plays a single note with the given guitar sound and the length in ms.
	 * 
	 * @param note
	 * @param guitar
	 * @param duration
	 *            the duration in milli-seconds
	 */
	public void compose(Note note, GuitarSound guitar, int noteValue) {

		composeSound(note, noteValue, MidiUtil.guitarToMidi(guitar));
	}

	private void composeSound(Sound sound, int noteValue, int instrument) {

		List<Stroke> noStrokes = Collections.emptyList();

		playSound(sound, noStrokes, noteValue, instrument);
	}

	private void playSound(Sound sound, List<Stroke> strokes, int noteValue,
			int instrument) {

		Play play;

		if (sound instanceof Chord) {

			ChordDefinition chord = ((Chord) sound).getChord();

			List<PlayableNote> notes = new ArrayList<PlayableNote>();

			int[] midis = new int[chord.getNotes().size()];
			for (int i = 0; i < midis.length; i++) {
				Note note = chord.getNotes().get(i);

				notes.add(createGuitarNote(note));
			}

			play = new PlayChord(notes, instrument, noteValue, NORMAL_VELOCITY);
		} else /* if (sound instanceof Note) */{

			Note note = (Note) sound;

			play = new PlayNote(createGuitarNote(note), instrument, noteValue,
					NORMAL_VELOCITY);
		}

		play.play(strokes);
	}

	public PlayableNote createGuitarNote(Note note) {

		int midi = MidiMapper.TypeRegistry.CACHE.getMidiFrom(note);
		int string = GitTabUtil.guitarString2Int(note.getGitString());

		return new PlayableNote(midi, string);
	}

	abstract class Play {

		/**
		 * May be changed in {@link #play(List)} because of eventually death
		 * notes.
		 */
		protected int instrument;
		private final int originalInstrument;

		protected final int noteValue;
		protected final int velocity;

		protected List<Stroke> strokes;

		public Play(int instrument, int noteValue, int velocity) {
			this.instrument = instrument;
			this.noteValue = noteValue;
			this.velocity = velocity;

			this.originalInstrument = instrument;
		}

		public void play(List<Stroke> strokes) {
			this.strokes = strokes;

			if (strokes.isEmpty()) {
				noStrokeGiven();
			}

			for (Stroke stroke : strokes) {

				if (stroke instanceof DownStroke) {

					// set instrument
					if (((DownStroke) stroke).isDeath()) {
						instrument = MidiUtil.effectToMidi(EffectSounds.MUTED);
					} else {
						instrument = originalInstrument;
					}

					playDownStroke();
				} else if (stroke instanceof UpStroke) {

					if (((UpStroke) stroke).isDeath()) {
						instrument = MidiUtil.effectToMidi(EffectSounds.MUTED);
					} else {
						instrument = originalInstrument;
					}

					playUpStroke();
				} else if (stroke instanceof Pause) {

					composer.pause(noteValue);
				} else if (stroke instanceof Hold) {
					// TODO hmm how to solve this?
				}
			}
		}

		/**
		 * Called when no {@link Stroke} is defined on a {@link Tone}.
		 */
		protected abstract void noStrokeGiven();

		/**
		 * Called when a down stroke shall be played.
		 */
		protected abstract void playDownStroke();

		/**
		 * Called when a up stroke shall be played.
		 */
		protected abstract void playUpStroke();
	}

	class PlayNote extends Play {

		private final PlayableNote note;

		public PlayNote(PlayableNote note, int instrument, int noteValue,
				int velocity) {

			super(instrument, noteValue, velocity);
			this.note = note;
		}

		@Override
		public void noStrokeGiven() {
			justPlay();
		}

		@Override
		public void playDownStroke() {
			justPlay();
		}

		@Override
		public void playUpStroke() {
			justPlay();
		}

		private void justPlay() {
			composer.note(note, noteValue, velocity, instrument);
		}
	}

	class PlayChord extends Play {

		private final List<PlayableNote> notes;

		public PlayChord(List<PlayableNote> notes, int instrument,
				int noteValue, int velocity) {

			super(instrument, noteValue, velocity);
			this.notes = notes;
		}

		@Override
		public void noStrokeGiven() {
			composer.chord(notes, noteValue, velocity, instrument);
		}

		@Override
		public void playDownStroke() {

			composer.chordPicked(notes, noteValue, velocity, PickingMode.DOWN,
					instrument);
		}

		@Override
		public void playUpStroke() {

			composer.chordPicked(notes, noteValue, velocity, PickingMode.UP,
					instrument);
		}
	}

	/**
	 * The correct duration value has to be calculated.
	 */
	private int getQuaterValue(NoteValues value) {
		return (int) Math.pow(2, value.getValue());
		// always calculates to an integer
	}
}
